using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace fuzzyKittenVNC
{
	public class VNCException : Exception {};
	public class VNCConnectionException : VNCException {};
	public class VNCInvalidServerException : VNCException {};
	public class VNCUnsupportedProtocolException : VNCException {};
	public class VNCSecurityTypeException : VNCException {
		public new readonly string Message = "No or unknown Security type was offered";
	}	
	/// <summary>
	/// 
	/// </summary>
	/// TODO: It would be best to decouple this from the Observable
	
	public class VNCClient : ObservableSubject, ISocketObservable
	{
		protected delegate void securityReadyHandlerDelegate();
		protected securityReadyHandlerDelegate securityReadyHandler;
		protected IPAddress vncAddress;
		protected Socket socket = null;
		protected System.Threading.Thread receiveThread;
		public string host = "localhost";
		public int port = 5403;
		public bool useTLS = false;
		public byte[] nullPackage = {0};
		public string versionString = "";
		public enum STATE {
			CLOSED = 0x00,
			CONNECTING = 0x01,
			CONNECTED = 0x02,
			ERROR = 0x03 
		}
		public VNCClient.STATE status = STATE.CLOSED;
		public VNCGenericRenderer renderer = null;
		public VNCClient.RFB_PROTOCOL_TYPE version;
		public VNCClientFramebuffer fb = null;
		
		public enum RFB_PROTOCOL_TYPE {
			RFB_3_3 = 0x01,
			RFB_3_7 = 0x02,
			RFB_3_8 = 0x03
		}
		
		
		public struct RFB_PROTOCOL {
			public static string RFB_3_3 = "RFB 003.003\n";
			public static string RFB_3_7 = "RFB 003.007\n";
			public static string RFB_3_8 = "RFB 003.008\n";
		}
		
		private VNCSecurity.SecImplementation securityHandler;
		
		public VNCClient (string host, int port, bool useTLS)
		{
			this.host = host;
			this.port = port;
			this.useTLS = useTLS;
		}
		
		private void setupConnection() {
			if(this.socket != null)
				this.socket.Close();
			this.socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
			IPHostEntry ipHostEntry = Dns.GetHostEntry(this.host);
			this.vncAddress = ipHostEntry.AddressList[0];
		}
		
		public void connect() {
			this.setupConnection();
			this.socket.Connect(new IPEndPoint(this.vncAddress,this.port));
			if(!this.socket.Connected) {
				this.status = VNCClient.STATE.ERROR;
				throw new VNCConnectionException();
			}
			this.status = VNCClient.STATE.CONNECTED;
			this.performHandshake();
			//this.communicate();
		}
		
		protected void performHandshake() {
			this.securityReadyHandler += this.performInitialisation;
			this.handleProtocolVersion();
			this.handleSecurityType();
		}
		
		protected void performInitialisation() {
			this.handleInitialisation();			
			this.requestUpdate(0,0,(uint) this.fb.format.frameBufferWidth,(uint) this.fb.format.frameBufferHeight,false);
			this.enableEncodings();
			this.setupThreads();
		}
		protected void setupThreads() {
			this.receiveThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.__receiveThreadStart));
			this.receiveThread.Start(this);
		}
		
		private void __receiveThreadStart() {
			try {
				new VNCClientReceiveThread(this,this.socket).receive();
			} catch(Exception e) {
				Logger.log(e.Message,Logger.LEVEL.ERROR);
			}
		}
		
		protected void handleProtocolVersion() {
			byte[] inBuffer = new byte[12];
			int bytesReceived;
			bytesReceived = this.socket.Receive(inBuffer,12,SocketFlags.None);
			string inASCII = Encoding.ASCII.GetString(inBuffer,0,12);
			if(bytesReceived != 12) 
				throw new VNCInvalidServerException();
			if(RFB_PROTOCOL.RFB_3_3 == inASCII) {
				this.version = RFB_PROTOCOL_TYPE.RFB_3_3;
				this.versionString = RFB_PROTOCOL.RFB_3_3;
			} else if (RFB_PROTOCOL.RFB_3_7 == inASCII) {
				this.version = RFB_PROTOCOL_TYPE.RFB_3_7;			
				this.versionString = RFB_PROTOCOL.RFB_3_7;	
			} else if (RFB_PROTOCOL.RFB_3_8 == inASCII) {
				this.version = RFB_PROTOCOL_TYPE.RFB_3_8;
				this.versionString = RFB_PROTOCOL.RFB_3_8;	
			} else {
				throw new VNCUnsupportedProtocolException();
			}
			inBuffer = Encoding.ASCII.GetBytes(this.versionString);
			this.socket.Send(inBuffer,12,SocketFlags.None);	

		}
		
		protected void handleSecurityType() {
			switch(this.version) {
				case RFB_PROTOCOL_TYPE.RFB_3_3:
					this.handleSecurityType_v3_3();
					break;
				case RFB_PROTOCOL_TYPE.RFB_3_7:
				case RFB_PROTOCOL_TYPE.RFB_3_8:
					this.handleSecurityType_v3_8();
					break;		
			}
			if(this.securityHandler == null)
				throw new VNCSecurityTypeException();
			if(this.securityHandler is VNCSecurity.PassSecImplementation) {
				string pass = String.Format("[{{ \"type\":\"PW_REQ\"}}]");
				this.renderer.sendDelegate(Encoding.ASCII.GetBytes(pass));
				
			} else {
				this.securityHandler.setup(this.socket,this.version);
				securityReadyHandler();
			}
		}
			                    
		protected void handleSecurityType_v3_3() {
			byte[] secType = new byte[4];
			this.socket.Receive(secType,4,SocketFlags.None);
			int secTypeNr = VNCClient.bytesToInt(secType);
			if(secTypeNr == 0)
				this.handleSecurityErrorMessage();
			else
				this.securityHandler = VNCSecurityHandler.getSecurityHandler(secTypeNr);
		}
		
		protected void enableEncodings() {
			VNCMessages.SetEncodings enc = new VNCMessages.SetEncodings();		
			securityHandler.send(enc.getMessage());	
		}
			
		protected void handleSecurityType_v3_8() {
			byte[] secTypeCountBuffer = new byte[1];
			byte[] secTypesBuffer;
			int[] secTypes;
			int secTypeCount; 

			this.socket.Receive(secTypeCountBuffer,1,SocketFlags.Partial);
			
			secTypeCount = Convert.ToInt32(secTypeCountBuffer[0]);
			if(secTypeCount == 0) {
				this.handleSecurityErrorMessage();
			} else {	
				secTypesBuffer = new byte[secTypeCount];
				secTypes = new int[secTypeCount];
				this.socket.Receive(secTypesBuffer,secTypeCount,SocketFlags.None);
				for(int i=0;i<secTypesBuffer.Length;i++) {
					secTypes[i] = Convert.ToInt32(secTypesBuffer[i]);
				}
				
				this.securityHandler = VNCSecurityHandler.getPreferredImplementation(secTypes);
			}
		}
		
		protected void handleSecurityErrorMessage() {
			byte[] msgLengthBuffer = new byte[4];
			Int32 msgLength;
			byte[] errorMsg;
			
			this.socket.Receive(msgLengthBuffer,4,SocketFlags.None);
			msgLength = VNCClient.bytesToInt(msgLengthBuffer);
			errorMsg = new byte[msgLength];

			this.socket.Receive(errorMsg,msgLength,SocketFlags.None);
			Logger.log(Encoding.UTF8.GetString(errorMsg),Logger.LEVEL.ERROR);
			throw new VNCSecurityTypeException();
		}
		
		protected void handleInitialisation() {
			byte[] serverInit = new byte[4096];
			string dimensions = "";
		//	int[] rawEnc = {0x00};
			this.socket.NoDelay = false;
			VNCMessages.ClientInit initMsg = new VNCMessages.ClientInit(true);

			VNCMessages.SetEncodings encodingMsg = new VNCMessages.SetEncodings();
		//	encodingMsg.encodings = rawEnc;
			
			securityHandler.send(initMsg.getMessage());	
			securityHandler.receive(serverInit);
			
			VNCMessages.ServerInit msg = VNCMessages.ServerInit.createFromStream(serverInit,this);
			// Allocate a new framebuffer
			this.fb = VNCClientFramebuffer.getInstanceForClient(this,msg);
			dimensions = String.Format(
					"[{{ \"type\":\"UD\", \"width\":{0}, \"height\":{1} }}]",
					fb.format.frameBufferWidth,fb.format.frameBufferHeight);
			this.renderer.sendDelegate(Encoding.ASCII.GetBytes(dimensions));
			
			// Send Encodings
			this.socket.Send(encodingMsg.getMessage());
		}
		
		// Will be called by a separate thread !
		public void handleMessage(VNCMessages.VNCServerMessage msg) {
			switch(msg.type) {
				
				case VNCMessages.VNCServerMessage.msgTypes.FRAMEBUFFERUPDATE:
					((VNCMessages.FrameBufferUpdate) msg).updateFramebufer();			
					this.renderer.renderDirtyRegions();

					break;
			}
		}
		
		public bool isReady() {
			
			return this.socket.Connected;
		}
		
		public void requestUpdateFromRequest(byte[] req) {
			string x = Encoding.UTF8.GetString(req);	
			string[] strings = x.Split(';');
			try {
				if(this.securityHandler != null) {		
					switch(strings[0]) {
						case "KillMe":
							if(this.receiveThread.IsAlive)
								break;
							this.receiveThread.Abort();	
							this.socket.Close();
							this.status = VNCClient.STATE.CLOSED;
							break;
						case "PW":
							this.securityHandler.setPassword(strings[1]);
							Logger.log("Password received",Logger.LEVEL.INFO);
							this.securityHandler.setup(this.socket,this.version);
							securityReadyHandler();
							break;
						case "FU":
							this.requestUpdate(0,0,(uint) this.fb.format.frameBufferWidth,(uint) this.fb.format.frameBufferHeight,strings[1] == "1");
							break;
						case "PE":
							VNCMessages.PointerEvent ptr = new VNCMessages.PointerEvent(
								Convert.ToInt32(strings[1]),Convert.ToInt32(strings[2]),Convert.ToByte(strings[3]));
							this.securityHandler.send(ptr.getMessage());
							break;
						case "KE":
							try {	
								VNCMessages.KeyEvent key = new VNCMessages.KeyEvent(strings[1] == "1",Convert.ToUInt32(strings[2]));
								Logger.log(String.Format("Key event {0} received - {1}",Convert.ToUInt32(strings[2]),strings[1] == "1" ? "Down" : "Up"),Logger.LEVEL.KEYS);	
								this.securityHandler.send(key.getMessage());
							} catch(Exception e ) {
								Console.Write("Key event ignored :{1} - {0}",strings[2],e.Message);
							} 
							break;
						case "CE":
							int[] encodings = new int[strings.Length];
							VNCMessages.SetEncodings enc = new VNCMessages.SetEncodings();
							try {
								for(int i=1;i<strings.Length-1;i++) {
									encodings[i-1] = Convert.ToInt32(strings[i]);
								}
							} catch(Exception e) {
								Logger.log(String.Format("Change encoding failed: {0}",e.Message),Logger.LEVEL.WARNING);
							} 
							enc.encodings = encodings;
							this.securityHandler.send(enc.getMessage());
							break;
					}
				} else {
					Logger.log(String.Format("Securityhandler not set up! Ignoring {0} ",strings[0]),Logger.LEVEL.ALERT);
				}			
			} catch(VNCConnectionException e) {
				this.notify();
			}
		}			
		
		public void requestUpdate(uint x,uint y, uint width, uint height, bool incremental) {			
			
			VNCMessages.FrameBufferUpdateRequest msg;
			msg = new fuzzyKittenVNC.VNCMessages.FrameBufferUpdateRequest(incremental,x,y,width,height);
			this.securityHandler.send(msg.getMessage());
		}
		
		public void disconnect() {
			if(this.socket != null)
				this.socket.Close();
		}
		
		public static int bytesToInt(byte[] byteArr) {
			return VNCClient.bytesToInt(byteArr,false);
		}
		
		public static int bytesToInt(byte[] byteArr,bool bigEndian) {
			int nr = 0;
			if(!bigEndian) {
				foreach(byte currentByte in byteArr) {
					nr <<= 8;
					nr |= Convert.ToInt32(currentByte);
				}
			} else {
				int i=0;
				foreach(byte currentByte in byteArr) {
					nr |= Convert.ToInt32(currentByte)<<i++*8;
				}
			}
			return nr;
		}
		
		public override Object getStatus() {
			return (Object) this.isReady();
		}
		
		public void shutdown() {
			this.socket.Close();
			this.status = VNCClient.STATE.CLOSED;
		}
	}
}

